package top.ruanlitao.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.ruanlitao.shortlink.project.common.biz.user.UserContext;
import top.ruanlitao.shortlink.project.common.constant.RedisKeyConstant;
import top.ruanlitao.shortlink.project.common.constant.ShortLinkConstant;
import top.ruanlitao.shortlink.project.common.convention.exception.ServiceException;
import top.ruanlitao.shortlink.project.dao.entity.*;
import top.ruanlitao.shortlink.project.dao.mapper.*;
import top.ruanlitao.shortlink.project.dto.biz.IpPlaceOfOriginDTO;
import top.ruanlitao.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import top.ruanlitao.shortlink.project.dto.req.ShortLinkStatsAccessRecordReqDTO;
import top.ruanlitao.shortlink.project.dto.req.ShortLinkStatsReqDTO;
import top.ruanlitao.shortlink.project.dto.resp.*;
import top.ruanlitao.shortlink.project.service.ShortLinkStatsService;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 短链接监控接口实现层
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ShortLinkStatsServiceImpl implements ShortLinkStatsService {

    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final GroupMapper groupMapper;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final ShortLinkMapper shortLinkMapper;

    private final RedissonClient redissonClient;
    private final StringRedisTemplate stringRedisTemplate;
    private final Searcher ip2reginSearcher;
    private final ThreadPoolTaskExecutor linkIpPlaceOfOriginExecutor;
    // 高德地图 API key
    @Value("${short-link.stats.locale.amap-key}")
    String aMapKey;

    @Override
    public ShortLinkStatsRespDTO oneShortLinkStats(ShortLinkStatsReqDTO requestParam) {
        // 验证当前用户和传递的 gid 是否匹配
        checkGroupBelongToUser(requestParam.getGid());

        // 获取以 日 为分组依据的 pv uv uip
        List<LinkAccessStatsDO> listStatsByShortLink = linkAccessStatsMapper.listStatsByShortLink(requestParam);
        if (CollUtil.isEmpty(listStatsByShortLink)) {
            return null;
        }

        // 一、总的基础监控信息（pv uv uip）
        LinkAccessStatsDO totalBaseStats = linkAccessLogsMapper.findPvUvUidStatsByShortLink(requestParam);

        // 二、每日的基础监控信息（pv uv uip）
        List<ShortLinkStatsAccessDailyRespDTO> dailyBaseStats = new ArrayList<>();
        // 构建一个包含了从起始日期到终止日期中所有日期的列表
        List<String> rangeDates = DateUtil.rangeToList(
                        DateUtil.parse(requestParam.getStartDate()),
                        DateUtil.parse(requestParam.getEndDate()),
                        DateField.DAY_OF_YEAR)
                .stream().map(DateUtil::formatDate).toList();
        // 遍历日期，在遍历过程中组装 ShortLinkStatsAccessDailyRespDTO 实体
        rangeDates.forEach(each -> listStatsByShortLink.stream()
                .filter(item -> Objects.equals(each, LocalDateTimeUtil.format(item.getDate(), "yyyy-MM-dd")))
                .findFirst()
                .ifPresentOrElse(item -> {
                    // 有记录的日子
                    ShortLinkStatsAccessDailyRespDTO shortLinkStatsAccessDailyRespDTO = ShortLinkStatsAccessDailyRespDTO.builder()
                            .date(each)
                            .pv(item.getPv())
                            .uv(item.getUv())
                            .uip(item.getUip())
                            .build();
                    dailyBaseStats.add(shortLinkStatsAccessDailyRespDTO);
                }, () -> {
                    // 没有记录的日子
                    ShortLinkStatsAccessDailyRespDTO shortLinkStatsAccessDailyRespDTO = ShortLinkStatsAccessDailyRespDTO.builder()
                            .date(each)
                            .pv(0)
                            .uv(0)
                            .uip(0)
                            .build();
                    dailyBaseStats.add(shortLinkStatsAccessDailyRespDTO);
                }));

        // 三、地区访问详情
        List<ShortLinkStatsLocaleCNRespDTO> localeStats = new ArrayList<>();
        List<LinkLocaleStatsDO> linkLocaleStatsDOList = linkLocaleStatsMapper.listLocaleByShortLink(requestParam);
        int localeSum = linkLocaleStatsDOList.stream().mapToInt(LinkLocaleStatsDO::getCnt).sum();
        linkLocaleStatsDOList.forEach(each -> {
            double ratio = ((double) each.getCnt()) / localeSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsLocaleCNRespDTO shortLinkStatsLocaleCNRespDTO = ShortLinkStatsLocaleCNRespDTO.builder()
                    .cnt(each.getCnt())
                    .locale(each.getProvince())
                    .ratio(actualRatio)
                    .build();
            localeStats.add(shortLinkStatsLocaleCNRespDTO);
        });

        // 四、小时的访问详情
        List<Integer> hourStats = new ArrayList<>();
        List<LinkAccessStatsDO> linkHourStatsList = linkAccessStatsMapper.listHourStatsByShortLink(requestParam);
        // linkAccessStatsDOList.forEach(System.out::println);
        for (int i = 0; i < 24; i++) {
            AtomicInteger hour = new AtomicInteger(i);
            Integer hourCnt = linkHourStatsList.stream()
                    .filter(each -> Objects.equals(hour.get(), each.getHour()))
                    .findFirst()
                    .map(LinkAccessStatsDO::getPv)
                    .orElse(0);
            hourStats.add(hourCnt);
        }

        // 五、高频IP访问详情
        List<ShortLinkStatsTopIpRespDTO> topIpStats = new ArrayList<>();
        List<HashMap<String, Object>> hashMaps = linkAccessLogsMapper.listTopIpByShortLink(requestParam);
        // hashMaps.forEach(System.out::println);
        hashMaps.forEach(each -> {
            ShortLinkStatsTopIpRespDTO shortLinkStatsTopIpRespDTO = ShortLinkStatsTopIpRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .ip(each.get("ip").toString())
                    .build();
            topIpStats.add(shortLinkStatsTopIpRespDTO);
        });

        // 六、一周访问详情
        List<Integer> weekdayStats = new ArrayList<>();
        List<LinkAccessStatsDO> listWeekdayStatsByShortLink = linkAccessStatsMapper.listWeekdayStatsByShortLink(requestParam);
        for (int i = 1; i < 8; i++) {
            AtomicInteger weekday = new AtomicInteger(i);
            int weekdayCnt = listWeekdayStatsByShortLink.stream()
                    .filter(each -> Objects.equals(each.getWeekday(), weekday.get()))
                    .findFirst()
                    .map(LinkAccessStatsDO::getPv)
                    .orElse(0);
            weekdayStats.add(weekdayCnt);
        }

        // 七、浏览器访问详情
        List<ShortLinkStatsBrowserRespDTO> browserStats = new ArrayList<>();
        List<HashMap<String, Object>> listBrowserStatsByShortLink = linkBrowserStatsMapper.listBrowserStatsByShortLink(requestParam);
        int browserSum = listBrowserStatsByShortLink.stream()
                .mapToInt(each -> Integer.parseInt(each.get("count").toString()))
                .sum();
        listBrowserStatsByShortLink.forEach(each -> {
            double ratio = (double) Integer.parseInt(each.get("count").toString()) / browserSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsBrowserRespDTO browserRespDTO = ShortLinkStatsBrowserRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .browser(each.get("browser").toString())
                    .ratio(actualRatio)
                    .build();
            browserStats.add(browserRespDTO);
        });

        // 八、操作系统访问详情
        List<ShortLinkStatsOsRespDTO> osStats = new ArrayList<>();
        List<HashMap<String, Object>> listOsStatsByShortLink = linkOsStatsMapper.listOsStatsByShortLink(requestParam);
        int osSum = listOsStatsByShortLink.stream()
                .mapToInt(each -> Integer.parseInt(each.get("count").toString()))
                .sum();
        listOsStatsByShortLink.forEach(each -> {
            double ratio = (double) Integer.parseInt(each.get("count").toString()) / osSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsOsRespDTO osRespDTO = ShortLinkStatsOsRespDTO.builder()
                    .cnt(Integer.parseInt(each.get("count").toString()))
                    .os(each.get("os").toString())
                    .ratio(actualRatio)
                    .build();
            osStats.add(osRespDTO);
        });

        // 九、访客访问类型详情
        List<ShortLinkStatsUvRespDTO> uvTypeStats = new ArrayList<>();
        HashMap<String, Object> findUvTypeByShortLink = linkAccessLogsMapper.findUvTypeCntByShortLink(requestParam);
        int oldUserCnt = Integer.parseInt(
                Optional.ofNullable(findUvTypeByShortLink)
                        .map(each -> each.get("oldUserCnt"))
                        .map(Object::toString)
                        .orElse("0")
        );
        int newUserCnt = Integer.parseInt(
                Optional.ofNullable(findUvTypeByShortLink)
                        .map(each -> each.get("newUserCnt"))
                        .map(Object::toString)
                        .orElse("0")
        );
        int uvSum = oldUserCnt + newUserCnt;
        double oldRatio = (double) oldUserCnt / uvSum;
        double actualOldRatio = Math.round(oldRatio * 100.0) / 100.0;
        double newRatio = (double) newUserCnt / uvSum;
        double actualNewRatio = Math.round(newRatio * 100.0) / 100.0;
        ShortLinkStatsUvRespDTO newUvRespDTO = ShortLinkStatsUvRespDTO.builder()
                .uvType("newUser")
                .cnt(newUserCnt)
                .ratio(actualNewRatio)
                .build();
        uvTypeStats.add(newUvRespDTO);
        ShortLinkStatsUvRespDTO oldUvRespDTO = ShortLinkStatsUvRespDTO.builder()
                .uvType("oldUser")
                .cnt(oldUserCnt)
                .ratio(actualOldRatio)
                .build();
        uvTypeStats.add(oldUvRespDTO);

        // 十、访问设备类型详情
        List<ShortLinkStatsDeviceRespDTO> deviceStats = new ArrayList<>();
        List<LinkDeviceStatsDO> listDeviceStatsByShortLink = linkDeviceStatsMapper.listDeviceStatsByShortLink(requestParam);
        int deviceSum = listDeviceStatsByShortLink.stream()
                .mapToInt(LinkDeviceStatsDO::getCnt)
                .sum();
        listDeviceStatsByShortLink.forEach(each -> {
            double ratio = (double) each.getCnt() / deviceSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsDeviceRespDTO deviceRespDTO = ShortLinkStatsDeviceRespDTO.builder()
                    .cnt(each.getCnt())
                    .device(each.getDevice())
                    .ratio(actualRatio)
                    .build();
            deviceStats.add(deviceRespDTO);
        });

        // 十一、访问网络类型详情
        List<ShortLinkStatsNetworkRespDTO> networkStats = new ArrayList<>();
        List<LinkNetworkStatsDO> listNetworkStatsByShortLink = linkNetworkStatsMapper.listNetworkStatsByShortLink(requestParam);
        int networkSum = listNetworkStatsByShortLink.stream()
                .mapToInt(LinkNetworkStatsDO::getCnt)
                .sum();
        listNetworkStatsByShortLink.forEach(each -> {
            double ratio = (double) each.getCnt() / networkSum;
            double actualRatio = Math.round(ratio * 100.0) / 100.0;
            ShortLinkStatsNetworkRespDTO networkRespDTO = ShortLinkStatsNetworkRespDTO.builder()
                    .cnt(each.getCnt())
                    .network(each.getNetwork())
                    .ratio(actualRatio)
                    .build();
            networkStats.add(networkRespDTO);
        });
        return ShortLinkStatsRespDTO.builder()
                .pv(totalBaseStats.getPv())
                .uv(totalBaseStats.getUv())
                .uip(totalBaseStats.getUip())
                .daily(dailyBaseStats)
                .localeCnStats(localeStats)
                .hourStats(hourStats)
                .topIpStats(topIpStats)
                .weekdayStats(weekdayStats)
                .browserStats(browserStats)
                .osStats(osStats)
                .uvTypeStats(uvTypeStats)
                .deviceStats(deviceStats)
                .networkStats(networkStats)
                .build();
    }

    @Override
    public IPage<ShortLinkStatsAccessRecordRespDTO> shortLinkStatsAccessRecord(ShortLinkStatsAccessRecordReqDTO requestParam) {
        // 验证当前用户和传递的 gid 是否匹配
        checkGroupBelongToUser(requestParam.getGid());

        LambdaQueryWrapper<LinkAccessLogsDO> queryWrapper = Wrappers.lambdaQuery(LinkAccessLogsDO.class)
                .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                .between(LinkAccessLogsDO::getCreateTime, requestParam.getStartDate(), requestParam.getEndDate())
                .eq(LinkAccessLogsDO::getDelFlag, 0)
                .orderByDesc(LinkAccessLogsDO::getCreateTime);
        IPage<LinkAccessLogsDO> linkAccessLogsDOIPage = linkAccessLogsMapper.selectPage(requestParam, queryWrapper);
        if (CollUtil.isEmpty(linkAccessLogsDOIPage.getRecords())) {
            return new Page<>();
        }
        IPage<ShortLinkStatsAccessRecordRespDTO> actualResult = linkAccessLogsDOIPage.convert(each -> BeanUtil.toBean(each, ShortLinkStatsAccessRecordRespDTO.class));
        /*List<String> userAccessLogsList = actualResult.getRecords().stream()
                .map(ShortLinkStatsAccessRecordRespDTO::getUser)
                .toList();
        List<Map<String, Object>> uvTypeList = linkAccessLogsMapper.selectUvTypeByUsers(
                requestParam.getGid(),
                requestParam.getFullShortUrl(),
                requestParam.getEnableStatus(),
                requestParam.getStartDate(),
                requestParam.getEndDate(),
                userAccessLogsList
        );
        actualResult.getRecords().forEach(each -> {
            String uvType = uvTypeList.stream()
                    .filter(item -> Objects.equals(each.getUser(), item.get("user")))
                    .findFirst()
                    .map(item -> item.get("uvType"))
                    .map(Object::toString)
                    .orElse("旧访客");
            each.setUvType(uvType);
        });*/
        List<ShortLinkStatsAccessRecordRespDTO> records = actualResult.getRecords();
        HashSet<String> userSet = new HashSet<>();
        for (int i = records.size() - 1; i >= 0; i--) {
            if (!userSet.contains(records.get(i).getUser())) {
                records.get(i).setUvType("新访客");
                userSet.add(records.get(i).getUser());
            } else {
                records.get(i).setUvType("旧访客");
            }
        }
        return actualResult;
    }

    @Override
    @Transactional
    public void actualSaveShortLinkStats(ShortLinkStatsRecordDTO shortLinkStatsRecord) {
        String fullShortUrl = shortLinkStatsRecord.getFullShortUrl();
        // 获取读锁（读锁可以一直获取，但当写锁被获取后，读锁就获取不到了）
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(RedisKeyConstant.LOCK_GID_UPDATE_KEY, fullShortUrl));
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        try {
            // 一、统计小时级别的 PV UV UIP
            LocalDateTime now = LocalDateTime.now();
            LocalDate date = LocalDate.now();
            Integer weekday = now.getDayOfWeek().getValue();
            Integer hour = now.getHour();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .date(date)
                    .pv(1)
                    // 根据 uvFirstFlag 来确定 uv 是 1 还是 0
                    .uv(shortLinkStatsRecord.getUvFirstFlag() ? 1 : 0)
                    // 根据 uipFirstFlag 来确定 uip 是 1 还是 0
                    .uip(shortLinkStatsRecord.getUipFirstFlag() ? 1 : 0)
                    .hour(hour)
                    .weekday(weekday)
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);

            // 二、统计地区
            IpPlaceOfOriginDTO ipPlaceOfOrigin = getIpPlaceOfOrigin(shortLinkStatsRecord, date);
            if (ipPlaceOfOrigin != null) {
                // 更新地区访问次数
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .date(date)
                        .cnt(1)
                        .province(ipPlaceOfOrigin.getProvince())
                        .city(ipPlaceOfOrigin.getCity())
                        .adcode("000000")
                        .country("中国")
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
            }

            // 三、统计OS
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .date(date)
                    .cnt(1)
                    .os(shortLinkStatsRecord.getOs())
                    .build();
            linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);

            // 四、统计浏览器
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .date(date)
                    .cnt(1)
                    .browser(shortLinkStatsRecord.getBrowser())
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);

            // 五、统计设备
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .date(date)
                    .cnt(1)
                    .device(shortLinkStatsRecord.getDevice())
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);

            // 六、统计网络
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .date(date)
                    .cnt(1)
                    .network(shortLinkStatsRecord.getNetwork())
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);

            // 七、记录访问日志
            if (ipPlaceOfOrigin != null) {
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .user(shortLinkStatsRecord.getUv())
                        .browser(shortLinkStatsRecord.getBrowser())
                        .os(shortLinkStatsRecord.getOs())
                        .ip(shortLinkStatsRecord.getRemoteAddr())
                        .network(shortLinkStatsRecord.getNetwork())
                        .device(shortLinkStatsRecord.getDevice())
                        .locale(StrUtil.join("-", "中国", ipPlaceOfOrigin.getProvince(), ipPlaceOfOrigin.getCity()))
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);
            }

            // 八、记录今日 PV、UV、UIP
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .date(date)
                    .todayPv(1)
                    .todayUv(shortLinkStatsRecord.getUvFirstFlag() ? 1 : 0)
                    .todayUip(shortLinkStatsRecord.getUipFirstFlag() ? 1 : 0)
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);

            // 九、link 表记录累计的 PV、UV、UIP
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(Wrappers.lambdaQuery(ShortLinkGotoDO.class).eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl));
            String gid = shortLinkGotoDO.getGid();
            shortLinkMapper.incrementStats(gid, fullShortUrl, 1, shortLinkStatsRecord.getUvFirstFlag() ? 1 : 0, shortLinkStatsRecord.getUipFirstFlag() ? 1 : 0);

            log.info("短链接 {} 监控数据加入到数据库了", shortLinkStatsRecord.getFullShortUrl());
        } finally {
            rLock.unlock();
        }
    }

    /**
     * 验证当前登录用户和传递过来的gid是否匹配
     */
    private void checkGroupBelongToUser(String gid) throws ServiceException {
        String username = Optional.ofNullable(UserContext.getUsername())
                .orElseThrow(() -> new ServiceException("用户未登录"));
        LambdaQueryWrapper<GroupDO> queryWrapper = Wrappers.lambdaQuery(GroupDO.class)
                .eq(GroupDO::getUsername, username)
                .eq(GroupDO::getGid, gid);
        List<GroupDO> groupDOList = groupMapper.selectList(queryWrapper);
        if (CollUtil.isEmpty(groupDOList)) {
            throw new ServiceException("用户信息与分组标识不匹配");
        }
    }

    private String getValueOrDefault(JSONObject jsonObject, String key) {
        String value = jsonObject.getString(key);
        return (StrUtil.isNotBlank(value) && Objects.equals("[]", value)) ? "未知" : value;
    }


    /**
     * 获取 IP 地址的归属地
     * @param shortLinkStatsRecord 纪录
     * @param date 日期
     * @return 归属地
     */
    private IpPlaceOfOriginDTO getIpPlaceOfOrigin(ShortLinkStatsRecordDTO shortLinkStatsRecord, LocalDate date) {
        // 1. 查询 redis 缓存
        String jsonPlace = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.IP_PLACE_OF_ORIGIN_KEY, shortLinkStatsRecord.getRemoteAddr()));
        if (StrUtil.isNotBlank(jsonPlace)) {
            JSONObject jsonObject = JSON.parseObject(jsonPlace);
            return IpPlaceOfOriginDTO.builder()
                    .ip(shortLinkStatsRecord.getRemoteAddr())
                    .country(jsonObject.getString("country"))
                    .province(jsonObject.getString("province"))
                    .city(jsonObject.getString("city"))
                    .build();
        }

        // 2. 查询本地数据库 ip2regin
        try {
            String regin = ip2reginSearcher.search(shortLinkStatsRecord.getRemoteAddr());
            String[] regins = regin.split("\\|");
            if (!StrUtil.equals("内网IP", regins[3]) || StrUtil.equalsAny("0", regins[0], regins[2], regins[3])) {
                // 3. 异步调用高地地图 api 获取 ip 归属地，最后在将归属地信息保存到 redis 中
                getIpPlaceOfOriginFromAPI(shortLinkStatsRecord, date);
                return null;
            }
            String country = StrUtil.equals("0", regins[0]) ? "未知" : regins[0];
            String province = StrUtil.equals("0", regins[2]) ? "未知" : regins[2];
            String city = StrUtil.equals("0", regins[3]) ? "未知" : regins[3];
            IpPlaceOfOriginDTO ipPlaceOfOriginDTO = IpPlaceOfOriginDTO.builder()
                    .ip(shortLinkStatsRecord.getRemoteAddr())
                    .country(country)
                    .province(province)
                    .city(city)
                    .build();
            // 将归属地信息保存到 redis 中
            stringRedisTemplate.opsForValue().set(
                    String.format(RedisKeyConstant.IP_PLACE_OF_ORIGIN_KEY,
                            shortLinkStatsRecord.getRemoteAddr()),
                    JSON.toJSONString(ipPlaceOfOriginDTO));
            return ipPlaceOfOriginDTO;
        } catch (Exception e) {
            // 3. 异步调用高地地图 api 获取 ip 归属地，最后在将归属地信息保存到 redis 中
            getIpPlaceOfOriginFromAPI(shortLinkStatsRecord, date);
            return null;
        }
    }

    /**
     * 获取 IP 地址的归属地并保存到数据库
     * @param shortLinkStatsRecord 纪录
     * @param date 日期
     */
    private void getIpPlaceOfOriginFromAPI(ShortLinkStatsRecordDTO shortLinkStatsRecord, LocalDate date) {
        Runnable task = () -> {
            IpPlaceOfOriginDTO ipPlaceOfOriginDTO = tryGetIpInfoNTimes(shortLinkStatsRecord.getRemoteAddr(), 3);
            if (ipPlaceOfOriginDTO != null) {
                // 更新地区访问次数
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .fullShortUrl(shortLinkStatsRecord.getFullShortUrl())
                        .date(date)
                        .cnt(1)
                        .province(ipPlaceOfOriginDTO.getProvince())
                        .city(ipPlaceOfOriginDTO.getCity())
                        .adcode("000000")
                        .country("中国")
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);

                // 插入访问记录
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .fullShortUrl(shortLinkStatsRecord.getFullShortUrl())
                        .user(shortLinkStatsRecord.getUv())
                        .browser(shortLinkStatsRecord.getBrowser())
                        .os(shortLinkStatsRecord.getOs())
                        .ip(shortLinkStatsRecord.getRemoteAddr())
                        .network(shortLinkStatsRecord.getNetwork())
                        .device(shortLinkStatsRecord.getDevice())
                        .locale(StrUtil.join("-", "中国", ipPlaceOfOriginDTO.getProvince(), ipPlaceOfOriginDTO.getCity()))
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);

                // 将归属地信息保存到 redis 中
                stringRedisTemplate.opsForValue().set(
                        String.format(RedisKeyConstant.IP_PLACE_OF_ORIGIN_KEY,
                                shortLinkStatsRecord.getRemoteAddr()),
                        JSON.toJSONString(ipPlaceOfOriginDTO));
            } else {
                log.warn("获取 IP 地址的归属地失败，ip: {}", shortLinkStatsRecord.getRemoteAddr());
            }
        };
        linkIpPlaceOfOriginExecutor.execute(task);
    }

    @SneakyThrows
    private IpPlaceOfOriginDTO tryGetIpInfoNTimes(String ip, int n) {
        for (int i = 0; i < n; i++) {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("key", aMapKey);
            paramMap.put("ip", ip);
            String responseJsonStr = HttpUtil.get(ShortLinkConstant.AMAP_REMOTE_URL, paramMap);
            JSONObject jsonObject = JSON.parseObject(responseJsonStr);
            String infocode = jsonObject.getString("infocode");
            if (StrUtil.isNotBlank(infocode) && Objects.equals("10000", infocode)) {
                String province = getValueOrDefault(jsonObject, "province");
                String city = getValueOrDefault(jsonObject, "city");
                return IpPlaceOfOriginDTO.builder()
                        .ip(ip)
                        .country("中国")
                        .province(province)
                        .city(city)
                        .build();
            } else {
                Thread.sleep(1000);
            }
        }
        return null;
    }
}